51 — 导入并优化场景
介绍
在之前的课程中,我们创建并导出了我们的场景。
现在我们可以把它导入到 Three.js 中,但我们还要尽可能地对模型进行优化。
设置
目前,我们的场景里只有一个白色立方体。
加载器已经添加到代码中了,而且已经支持 Draco:
/**
* Loaders
*/
// Texture loader
const textureLoader = new THREE.TextureLoader()
// Draco loader
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('draco/')
// GLTF loader
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader) portal.glb 和 baked.jpg 文件位于 /static/ 文件夹中,不过你可以用你自己的文件替换它们。
同时还有一个 lil-gui 实例可用,我们会在下一节课中给它添加一些调节项。
抗锯齿(antialias)已经在 WebGLRenderer 上设置好了。
加载模型
我们可以先从加载模型开始。我们会把立方体保留在场景中,以确保一切正常工作。一旦我们能看到传送门场景,我们就会把立方体移除。
在加载器部分之后加载模型,并在控制台中测试结果:
/**
* Model
*/
gltfLoader.load(
'portal.glb',
(gltf) =>
{
console.log(gltf.scene)
}
) 如果你没有在控制台日志中看到这个场景,请检查路径并查找错误。
现在我们来尝试把模型添加到场景中:
gltfLoader.load(
'portal.glb',
(gltf) =>
{
scene.add(gltf.scene)
}
)
你应该能看到一些轮廓剪影。
问题在于,默认情况下,当 GLTF 导出 PBR 材质时,它们会被解释为 MeshStandardMaterial。而且,这种材质需要光照。
但我们不想要灯光,因为我们的场景是 baked(烘焙)过的,所有光照和阴影信息都已经包含在 texture 中了:
所以,我们要创建一个 MeshBasicMaterial 并将它应用到场景中的所有元素上。
由于我们要创建多个 material,我们需要在加载模型之前,在代码中为它们创建一个区域:
/**
* Materials
*/
// Baked material
const bakedMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }) 场景中有很多对象。与其手动把 material 应用到每一个对象上,我们可以在加载回调中遍历(traverse)场景:
gltfLoader.load(
'portal.glb',
(gltf) =>
{
gltf.scene.traverse((child) =>
{
child.material = bakedMaterial
})
scene.add(gltf.scene)
}
)
你应该能看到所有对象都显示出统一的红色。
现在,移除与白色立方体相关的代码。
加载 texture
要加载 texture,我们可以使用代码中已有的 textureLoader。
在 materials 之前加载它:
/**
* Textures
*/
const bakedTexture = textureLoader.load('baked.jpg') 确保日志中没有出现任何错误。
要把这个 texture 应用到我们的场景上,我们可以通过 map 属性把它添加到 bakedMaterial 中:
const bakedMaterial = new THREE.MeshBasicMaterial({ map: bakedTexture })
虽然这看起来很有意思也很好笑,但这并不是我们想要的效果。
问题在于,我们 texture 的 Y 坐标被反转了。就好像我们的 texture 被垂直镜像了一样。
在软件 / 库之间,对于 texture 坐标中 Y 轴的方向存在分歧。在上面的图示中我们不太走运,但我们只需把 texture 翻转过来即可。
为此,把 bakedTexture 的 flipY 属性设置为 false:
bakedTexture.flipY = false
现在场景看起来好多了。
我们仍然需要处理灯柱灯和传送门。而且与 Blender 渲染相比,颜色看起来有些褪色。
修复颜色
我们的 baked texture 是用 sRGB 编码的,但 Three.js 并不知道这一点。
要设置 texture 的编码,把它的 colorSpace 属性改为 THREE.SRGBColorSpace:
bakedTexture.colorSpace = THREE.SRGBColorSpace
现在的颜色与 Blender 渲染中的完全一致。
修复发光对象
是时候处理灯柱灯和传送门了。
因为它们是自发光(emission)材质,应该看起来像是统一的亮色,这就是为什么我们可以使用 MeshBasicMaterial。
为灯柱灯创建一个 MeshBasicMaterial:
// Pole light material
const poleLightMaterial = new THREE.MeshBasicMaterial({ color: 0xffffe5 }) 现在的问题是,我们要如何把这个 material 应用到场景中正确的对象上。
如果你在加载回调中打印出场景的 children,你会看到其中有 56 个 Meshes(这个数字可能会因你的模型而不同):
gltfLoader.load(
'portal.glb',
(gltf) =>
{
console.log(gltf.scene.children)
// ...
}
) 要解决如何把正确的 material 应用到正确的对象上,我们要编辑模型,为每个发光对象设置一个特定的名称。
在 Blender 中打开模型(使用你的 .blend 文件,或使用位于 /resources/ 文件夹中的文件):
启用 emissions 集合的选择(如果还没有启用的话):
相应地重命名每一个对象:
poleLightApoleLightBportalLight
在 Blender 中,你不能给多个对象使用相同的名称,因为每个对象都有唯一的 ID。
选中所有内容(camera 和 light 除外)并将其导出为 glTF 2.0:
使用与上一节课相同的参数,并替换 /static/ 文件夹中的 portal.glb 文件:
剧透提醒:不要关闭 Blender!
在 Three.js 中,你可以通过 name 属性访问对象的名称:
gltfLoader.load(
'portal.glb',
(gltf) =>
{
console.log(gltf.scene.children[0].name)
// ...
}
) 现在我们能做的是,通过使用 name 属性在 gltf.scene.children 数组中进行查找。
在 JavaScript 中,你可以用 find(...) 方法在数组中“查找”:
gltfLoader.load(
'portal.glb',
(gltf) =>
{
gltf.scene.traverse((child) =>
{
child.material = bakedMaterial
})
scene.add(gltf.scene)
// Get each object
const portalLightMesh = gltf.scene.children.find((child) => child.name === 'portalLight')
const poleLightAMesh = gltf.scene.children.find((child) => child.name === 'poleLightA')
const poleLightBMesh = gltf.scene.children.find((child) => child.name === 'poleLightB')
console.log(portalLightMesh)
console.log(poleLightAMesh)
console.log(poleLightBMesh)
}
) 你应该能在控制台中看到这 3 个对象中的每一个。
如果你遇到错误或得到 undefined,请检查你的 JavaScript 以及 Blender 场景中的名称。此外,请确保你以正确的方式导出了模型。
现在我们可以用之前创建的 material 替换灯柱灯:
gltfLoader.load(
'portal.glb',
(gltf) =>
{
gltf.scene.traverse((child) =>
{
child.material = bakedMaterial
})
scene.add(gltf.scene)
// Get each object
const portalLightMesh = gltf.scene.children.find((child) => child.name === 'portalLight')
const poleLightAMesh = gltf.scene.children.find((child) => child.name === 'poleLightA')
const poleLightBMesh = gltf.scene.children.find((child) => child.name === 'poleLightB')
// Apply materials
poleLightAMesh.material = poleLightMaterial
poleLightBMesh.material = poleLightMaterial
}
)
你应该能看到你的 MeshBasicMaterial 应用在两个灯上。
为传送门创建一个 MeshBasicMaterial 并以同样的方式应用它:
// Portal light material
const portalLightMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff })
// ...
gltfLoader.load(
'portal.glb',
(gltf) =>
{
gltf.scene.traverse((child) =>
{
child.material = bakedMaterial
})
scene.add(gltf.scene)
// Get each object
const portalLightMesh = gltf.scene.children.find((child) => child.name === 'portalLight')
const poleLightAMesh = gltf.scene.children.find((child) => child.name === 'poleLightA')
const poleLightBMesh = gltf.scene.children.find((child) => child.name === 'poleLightB')
// Apply materials
portalLightMesh.material = portalLightMaterial
poleLightAMesh.material = poleLightMaterial
poleLightBMesh.material = poleLightMaterial
}
)
如果你的传送门颜色不是白色,不要花太多时间尝试找到正确的值,因为我们稍后会用一个 ShaderMaterial 来替换它。
提升性能
我们本可以就此停下,但我们犯了一些性能上的错误。
首先,我们可以用 Spector.js 测试 draw call(绘制调用)。
监控
如果你还记得性能那一课,Spector.js 是一个 Chrome 插件,它让我们能看到 GPU 为创建渲染所经历的不同步骤。
在撰写本课时,这个 Chrome 插件已经不再可用了。
你不需要安装这个插件也能跟着下面的步骤操作,但时刻关注你的场景是如何被渲染的,以便对它进行优化,这总是好的。
如果你在 Chrome 中安装了 Spector.js,点击扩展图标来激活它,再点击一次打开以下菜单:
点击红色圆圈开始处理渲染。这可能需要几秒钟。
处理完成后,应该会打开一个窗口,里面包含我们需要的所有细节。在左侧,你可以滚动查看渲染场景所采取的不同步骤。步骤越少越好:
性能可能已经足够好了,但如果你仔细想想,我们应该只有 4 个对象:
- 第一个灯柱灯
- 第二个灯柱灯
- 传送门光
- 所有 baked 对象
我们不需要分离 baked 对象,因为我们很确定不会移动它们。例如,如果我们移动了斧头,我们仍然会在树干上看到它的阴影。如果我们移动了栅栏,我们仍然会在地板上看到它的阴影。
为了提升性能,我们要合并这些 geometry。
合并 baked 对象
回到 Blender:
我们不想影响发光对象。确保把 emissions 集合设置为不可选中:
现在就把所有 geometry 合并会是个坏主意,因为之后我们可能就无法修改场景了。我们要先把它们复制一份,把所有内容放进另一个集合,然后再合并。
创建一个空的 merged 集合:
选中场景中的所有对象(不包括 camera、light 和发光对象),然后用 SHIFT + D 复制它们。在场景中点击一次(不要移动鼠标)来确认复制。
在这些复制出来的对象仍被选中时,按 M 把它们移动到 merged 集合中:
现在所有对象都已经复制到 merged 集合中了。你可以按 CTRL + J(Windows)或 CMD + J(MacOS)来合并它们。
停用所有其他集合,并检查你只有一个合并后的对象:
把这个单一对象的名字改为 baked:
重新启用 emissions 集合,以便能够选中其中的对象。
你应该保留你最初创建的集合,即使它们被停用了。这样万一我们需要做修改或重新 bake 场景时它们还在:
如果你切换到 Renderer 着色模式,你会看到这些 material 在合并后的对象上仍然有效。
这是因为 Blender 可以在一个 geometry 上处理多个 material。这不成问题,因为在导出时,我们让 Blender 不要导出 material。但如果你好奇想看没有 material 的场景是什么样子,在选中 baked 对象时进入 Materials Properties 标签页,然后点击 - 图标删除所有 material:
这应该会得到一个好看的白色场景。
现在我们可以用与之前相同的参数导出合并后的对象和发光对象:
你应该会在 Three.js 中得到完全相同的结果:
但让我们看看 Spector.js:
如你所见,我们的场景只用了 4 个步骤就被绘制出来。一个用于 baked 对象,另外每个发光灯各一个。
我们本可以合并两个灯柱灯,但就目前而言这已经足够了。
现在我们只有一个对象需要应用 baked texture,就不再需要遍历已加载场景的所有子对象了。
我们可以用与发光对象相同的方法来查找它:
gltfLoader.load(
'portal.glb',
(gltf) =>
{
scene.add(gltf.scene)
// Get each object
const bakedMesh = gltf.scene.children.find((child) => child.name === 'baked')
const portalLightMesh = gltf.scene.children.find((child) => child.name === 'portalLight')
const poleLightAMesh = gltf.scene.children.find((child) => child.name === 'poleLightA')
const poleLightBMesh = gltf.scene.children.find((child) => child.name === 'poleLightB')
// Apply materials
bakedMesh.material = bakedMaterial
portalLightMesh.material = portalLightMaterial
poleLightAMesh.material = poleLightMaterial
poleLightBMesh.material = poleLightMaterial
}
)
你应该会得到相同的结果。如果没有,请确保你把合并后的对象名字改成了 baked,并且导出了它以及那 3 个发光对象。
在下一节课中,我们会给场景添加一些细节。